# -*- coding: utf-8 -*-

"""
Machine shop example

Covers:

- Interrupts
- Resources: PreemptiveResource（抢占式的，有些工作会抢服务）

Scenario:

  一个车间有 n 台相同的机器。一系列的工作（足以让机器保持繁忙）到达。
  每台机器定期发生故障。维修工对其进行维修。维修工还有其他不太重要的
  任务要完成。故障的机器会中断这些任务。维修工在完成机器维修后继续这
  些任务。车间持续运作。
 
  这是一个顾客源有限的模型，服务完成后顾客自动回到顾客源。
"""

# In[0]:
# 导入工具包库
# 数据分析工具
import numpy as np
import pandas as pd

# 数据可视化工具
import matplotlib.pyplot as plt
from pylab import mpl
# 指定默认字体：解决plot不能显示中文问题
mpl.rcParams['font.sans-serif'] = ['Microsoft YaHei'] 
mpl.rcParams['axes.unicode_minus'] = False 
# 解决保存图像是负号'-'显示为方块的问题

# 海出生工具包，是 matplotlib 的高级封装
# 用于数据可视化的美化
import seaborn as sns 

import random

import simpy

# In[1]:
#设置参数
RANDOM_SEED = 42
PT_MEAN = 10.0         # 任务处理平均时间（分钟
PT_SIGMA = 2.0         # 任务处理时间标准差（分钟
MTTF = 300.0           # 机器到故障的平均时间（分钟
BREAK_MEAN = 1 / MTTF  # 机器发生故障的概率用于负指数分布
REPAIR_TIME = 30.0     # 修理机器所需时间（分钟）
JOB_DURATION = 30.0    # 其他任务的时间（分钟）
NUM_MACHINES = 10      # 车间机器数量
WEEKS = 1              # 模拟时间（周）
NUM_REPAIRMAN = 1      # 修理工的人数
SIM_TIME = 3000        # 模拟时间（分钟）

# In[2]
def time_per_part():
    """ Return actual processing time for a concrete part.
        用正态分布生成机器生产一个零件所需要的真实时间

        参数：无，直接返回时间
    """
    return random.normalvariate(PT_MEAN, PT_SIGMA)
    # 正态分布有两个参数
    # 均值和方差

def time_to_failure():
    """Return time until next failure for a machine."""
    return random.expovariate(BREAK_MEAN) #？
# 用负指数分布生成机器故障间隔时间，因此机器是顾客，每次坏了就产生一个顾客

class MachineShopSimulator:
    def __init__(
            self, 
            env,
            num_machine, 
            num_repairman
            ):

        self.env = env
        self.queue_len_list = [
                [], # 时间戳
                []  # 各个时间下的队列长度
            ]
        
        self.repairman = repairman = simpy.PreemptiveResource(
                self.env, capacity=num_repairman)  
        # PreemptiveResource() 类：允许抢占资源的服务台类
        # capacity 为服务台容量
        # capacity=2 即可以同时服务两个顾客
        # 可以简单理解为多服务台 

        # 实例化机器对象
        # 这里用到了一个列表推导式，一次性创建了 NUM_MACHINES 台机器
        # 把这些机器一次性放进一个列表，统一管理。
        self.machine_list = [
                Machine(
                    self.env, 'Machine %d' % i, self.repairman
                    ) for i in range(num_machine)
                ]
        
        self.parts_made_list = [
            [] for machine in self.machine_list
        ]

        self.env.process(
                self.other_jobs()
                )
        self.env.process(
            self.record_queue_length()
        )
        self.env.process(
            self.record_parts_made()
        )

    def other_jobs(self):  
        """ repairman除了修理机器以外的工作
        """
        while True:
            # Start a new job
            done_in = JOB_DURATION
            while done_in:
                # Retry the job until it is done.
                # It's priority is lower than that of machine repairs.
                with self.repairman.request(
                        priority=2 # 低的优先级
                        ) as req:  
                    yield req
                    try:
                        start = self.env.now
                        yield self.env.timeout(done_in)
                        done_in = 0
                    except simpy.Interrupt:
                        done_in -= self.env.now - start

    def result(self):
        queue_info_df = pd.DataFrame(
            np.array(self.queue_len_list).T,
            columns=['time', 'queue_len']
        )
        return queue_info_df
    
    def record_queue_length(self):
        """ 记录队列长度
        """
        while True:
            self.queue_len_list[0].append(self.env.now)
            self.queue_len_list[1].append(
                len(self.repairman.queue)
                )
            yield self.env.timeout(1)  # 每隔一秒时间记录一次

    def record_parts_made(self):
        while True:
            for i in range(
                len(self.machine_list)
                ):
                self.parts_made_list[i].append(
                    self.machine_list[i].parts_made
                    )
            yield self.env.timeout(1)

# In[3]:
class Machine(object):
    """ A machine produces parts and my get broken every now and then.
     
        If it breaks, it requests a *repairman* and continues the production
        after the it is repaired.

        A machine has a *name* and a numberof *parts_made* thus far.
    """
    def __init__(self, env, name, repairman):
        self.env = env
        self.name = name 
        self.parts_made = 0 # 默认机器被创建出来的时候，一个零件也没生产
        self.broken = False # 初始化一个实例变量，因为最开始都没坏所以是f

        # Start "working" and "break_machine" processes for this machine.
        # 初始化的时候直接进入进程 break_machine
        # 不建议这样设计，因为逻辑混乱
        # 建议将初始化进程封装到主实践循环里面
        self.process = env.process(
                self.working(repairman)
                )
        env.process(
                self.break_machine()
                )

    def working(self, repairman): 
        """ Produce parts as long as the simulation runs.
            只要仿真在运行就一直生产零件

            While making a part, the machine may break multiple times.
            Request a repairman when this happens.
        """
        while True: # 主事件循环
            # 开始生产零件
            done_in = time_per_part()
            while done_in: 
                """
                    这一段的逻辑有点莫名其妙，我来解释一下：

                    首先，要理解几个问题。就是创建仿真进程的 env.process()
                    这个方法会返回一个 process 对象，而这个返回的 process 
                    对象又有一个方法，叫做 process.interrupt()，这个方法的
                    作用就是中断这个 process。这个中断是在 break_machine()
                    里面解释的。转到 break_machine() 方法去看，就可以知道
                    发生了什么。
                """
                try:
                    # Working on the part
                    start = self.env.now
                    yield self.env.timeout(done_in) # 等待零件完成
                    done_in = 0  # Set to 0 to exit while loop.

                except simpy.Interrupt:
                    self.broken = True
                    done_in -= self.env.now - start  # How much time left?
                    # 出现故障暂停制作的零件还差多久做完↑

                    # Request a repairman. This will preempt its "other_job"
                    with repairman.request(priority=1) as req:  
                        yield req
                        yield self.env.timeout(
                            random.expovariate(1.0 / REPAIR_TIME)
                            )   #修理时间

                    self.broken = False

            # Part is done.
            self.parts_made += 1

    def break_machine(self): #第二个进程坏
        """ Break the machine every now and then.
            间隔一段时间就把机器搞坏

            注释：
        """
        while True:
            yield self.env.timeout(time_to_failure())
            if not self.broken:
                # Only break the machine 
                # if it is currently working.
                self.process.interrupt()

# In[4]:
# Setup and start the simulation
print('Machine shop')
random.seed(RANDOM_SEED)  # This helps reproducing the results

# 创建环境并开始启动仿真
env = simpy.Environment()
simMachineShop = MachineShopSimulator(
    env,
    NUM_MACHINES, NUM_REPAIRMAN
)

# In[6]:
# Execute!
env.run(until=SIM_TIME)

# In[7]:
# Analyis/results
# print('Machine shop results after %s weeks' % WEEKS)
# for machine in machines:
#     print(
#         '%s made %d parts.' % (
#             machine.name, machine.parts_made
#             )
#         )

# In[8]:
fig, ax = plt.subplots(2, 1, dpi=300, figsize=(10, 10))
fig.suptitle('零件生产仿真结果')
sns.lineplot(
    simMachineShop.queue_len_list[1],
    ax=ax[0])
ax[0].set_xlabel('时间（分钟）')
ax[0].set_ylabel('队列长度')
for i in range(
    len(simMachineShop.parts_made_list)
    ):
    ax[1].plot(
        simMachineShop.parts_made_list[i]
        )
ax[1].set_title('零件制造进度')
ax[1].set_ylabel('生产的零件数')
ax[1].set_xlabel('时间（分钟）')
fig.subplots_adjust()